/*===========================================================================
  Copyright (C) 2014 by the Okapi Framework contributors
-----------------------------------------------------------------------------
  This library is free software; you can redistribute it and/or modify it 
  under the terms of the GNU Lesser General Public License as published by 
  the Free Software Foundation; either version 2.1 of the License, or (at 
  your option) any later version.

  This library is distributed in the hope that it will be useful, but 
  WITHOUT ANY WARRANTY; without even the implied warranty of 
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License 
  along with this library; if not, write to the Free Software Foundation, 
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

  See also the full LGPL text here: http://www.gnu.org/copyleft/lesser.html
===========================================================================*/

package net.sf.okapi.v2.common.om.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import net.sf.okapi.v2.common.C;
import net.sf.okapi.v2.common.U;
import net.sf.okapi.v2.common.exceptions.InvalidParameterException;
import net.sf.okapi.v2.common.exceptions.InvalidPositionException;
import net.sf.okapi.v2.common.om.ICode;
import net.sf.okapi.v2.common.om.IMarker;
import net.sf.okapi.v2.common.om.ITag;
import net.sf.okapi.v2.common.om.ITags;
import net.sf.okapi.v2.common.om.ITextContainer;
import net.sf.okapi.v2.common.om.ITextFragment;
import net.sf.okapi.v2.common.om.TagType;

/**
 * Implements {@link ITextFragment}.
 */
public class TextFragment implements ITextFragment {

	final private ITextContainer container;
	
	private String id;
	private ITags tags;
	private StringBuilder ct;
	
	/**
	 * Creates an empty new TextFragment.
	 * @param container the parent of this fragment.
	 */
	public TextFragment (ITextContainer container) {
		this.container = container;
		tags = container.getTags();
		ct = new StringBuilder();
	}
	
	/**
	 * Copy constructor.
	 * <p>For the tags: opening and closing tags are copied to the new fragment in the order they
	 * appear in the original fragment. a corresponding closing/opening tag (i.e. with the same id) is
	 * searched on the destination container. If it is found, the copied code is linked to it. If none
	 * is found an orphan tag is created.
	 * @param container the container where the new fragment will be set.
	 * @param original the original object to copy.
	 */
	public TextFragment (ITextContainer container,
		ITextFragment original)
	{
		this.container = container;
		tags = container.getTags();
		this.id = original.getId();
		
		// Copy the content
		ITags oriTags = original.getTags();
		StringBuilder tmp = new StringBuilder(original.getCodedText());
		int key;
		for ( int i=0; i<tmp.length(); i++ ) {
			char ch = tmp.charAt(i);
			if ( U.isChar1(ch) ) {
				if ( ch == C.PCONT_STANDALONE ) {
//					PMarker pm = oriMarkers.getPMarker(tmp, i);
//					StringBuilder pmtmp = new StringBuilder(pm.getCodedText());
//					for ( int j=0; j<pmtmp.length(); j++ ) {
//						char pmch = pmtmp.charAt(j);
//						if ( isMarker(pmch) ) {
//							BaseMarker bm = oriMarkers.get(pmtmp, j);
//							key = markers.add(CloneFactory.create(bm));
//							pmtmp.replace(j, j+2, toRef(key)); j++;
//						}
//					}
//					key = markers.add(new PMarker(pmtmp.toString()));
//					tmp.replace(i, i+2, toRef(key)); i++;
				}
				else {
					ITag tag = oriTags.get(U.toKey(ch, tmp.charAt(i+1)));
					// Create copy. for opening and closing codes we must search for their counterpart
					// so we can connect the two boundaries.
					if ( tag instanceof StartCode ) {
						// Look for closing counterpart
						ITag complement = container.getTags().getClosingTag(tag.getId());
						key = tags.add(new StartCode((StartCode)tag, (EndCode)complement));
					}
					else if ( tag instanceof EndCode ) {
						// Look for opening counterpart
						ITag complement = container.getTags().getOpeningTag(tag.getId());
						key = tags.add(new EndCode((EndCode)tag, (StartCode)complement));
					}
					else if ( tag instanceof LoneCode ) {
						key = tags.add(new LoneCode((LoneCode)tag));
					}
					else {
						throw new RuntimeException("Unable to clone tag of type "+tag.getClass().getName());
					}
					tmp.replace(i, i+2, U.toRef(key)); i++;
				}
			}
		}
		setCodedText(tmp.toString());
	}

	@Override
	public String toString () {
		return ct.toString();
	}

	@Override
	public char charAt (int index) {
		return ct.charAt(index);
	}

	@Override
	public int length () {
		return ct.length();
	}

	@Override
	public String subSequence (int start,
		int end)
	{
		if ( end == -1 ) end = ct.length();
		checkPosition(start);
		checkPosition(end);
		return ct.subSequence(start, end).toString();
	}

	@Override
	public String getId () {
		return id;
	}

	@Override
	public void setId (String id) {
		//TODO: should we check uniqueness?
		this.id = id;
	}

	@Override
	public boolean isSegment () {
		return false;
	}

	@Override
	public boolean isEmpty () {
		return (ct.length() == 0);
	}

	@Override
	public boolean hasText () {
		return hasText(false);
	}

	@Override
	public boolean hasText (boolean whitespaceIsText) {
		for ( int i=0; i<ct.length(); i++ ) {
			char ch = ct.charAt(i);
			if ( U.isChar1(ch) ) {
				i++;
			}
			else {
				// If we count whitespace as text, then we have text
				if ( whitespaceIsText ) return true;
				// Otherwise we have text if it's not a whitespace
				if ( !Character.isWhitespace(ch) ) return true;
			}
		}
		return false;
	}

	@Override
	public String getCodedText () {
		return ct.toString();
	}
	
	@Override
	public void setCodedText (String codedText) {
		ct = new StringBuilder(codedText);
	}

	/**
	 * Gets a plain text version (all marker references stripped out) of the content of this fragment.
	 * @return the plain text version of this fragment.
	 */
	public String getPlainText () {
		return C.TAGREF_REGEX.matcher(ct.toString()).replaceAll("");
	}

	@Override
	public String getText () {
		// Try fastest way first
		if ( tags.size() == 0 ) {
			return ct.toString();
		}
		// If there may be tags: build the string
		StringBuilder tmp = new StringBuilder(ct.length());
		for ( int i=0; i<ct.length(); i++ ) {
			char ch = ct.charAt(i);
			switch ( ch ) {
			case C.CODE_OPENING:
			case C.CODE_CLOSING:
			case C.CODE_STANDALONE:
				ICode code = (ICode)tags.get(U.toKey(ch, ct.charAt(++i)));
				if ( code.getData() != null ) tmp.append(code.getData());
				break;
			case C.MARKER_OPENING:
			case C.MARKER_CLOSING:
				i++; // Skip it
				break;
			case C.PCONT_STANDALONE:
				i++; // Skip it
			default:
				tmp.append(ch);
				break;
			}
		}
		return tmp.toString();
	}

	@Override
	public ITextContainer getParent () {
		return container;
	}
	
	@Override
	public boolean hasTag () {
		if ( tags.size() == 0 ) return false;
		// Else check the tag references
		// (the tags object is shared across the whole unit)
		for ( int i=0; i<ct.length(); i++ ) {
			if ( U.isChar1(ct.charAt(i)) ) return true;
		}
		return false;
	}

	@Override
	public ITags getTags () {
		return tags;
	}

	@Override
	public List<ITag> getOwnTags () {
		if ( tags.size() == 0 ) return Collections.emptyList();
		ArrayList<ITag> list = new ArrayList<>();
		for ( int i=0; i<ct.length(); i++ ) {
			if ( U.isChar1(ct.charAt(i)) ) {
				list.add(tags.get(U.toKey(ct.charAt(i), ct.charAt(++i))));
			}
		}
		return list;
	}

	@Override
	public void clear () {
		// Removes all the tags
		for ( int i=0; i<ct.length(); i++ ) {
			char ch = ct.charAt(i);
			if ( U.isChar1(ch) ) {
				tags.remove(U.toKey(ch, ct.charAt(++i)));
			}
		}
		// New coded text
		ct = new StringBuilder();
	}

	@Override
	public void delete (int start,
		int end)
	{
		if ( end == -1 ) end = ct.length();
		checkPosition(start);
		checkPosition(end);
		// Removes all the tags
		for ( int i=start; i<end; i++ ) {
			char ch = ct.charAt(i);
			if ( U.isChar1(ch) ) {
				tags.remove(U.toKey(ch, ct.charAt(++i)));
			}
		}
		ct.delete(start, end);
	}

	@Override
	public ITextFragment append (char ch) {
		ct.append(ch);
		return this;
	}

	@Override
	public ITextFragment append (CharSequence plainText) {
		// StringBuilder is an Appendable so null is process as expected
		ct.append(plainText);
		return this;
	}

	@Override
	public ITextFragment append (CharSequence plainText,
		int start,
		int end)
	{
		// StringBuilder is an Appendable so null is process as expected
		ct.append(plainText.subSequence(start, end));
		return this;
	}

	@Override
	public ICode insert (TagType tagType,
		String type,
		String id, 
		String data,
		int offset,
		boolean connect,
		boolean allowOrphan)
	{
		// Check ID
		if ( id == null ) {
			if ( connect && !allowOrphan ) {
				throw new InvalidParameterException(
					"Cannot have auto-generated ID when requesting to link to an existing code and not allowing orphan.");
			}
			// Generate an Id
			id = tags.getStore().suggestId(false);
		}
		// Check/adjust the offset
		if (( offset < 0 ) || ( offset > ct.length() )) {
			offset = -1; // append at the end
		}
		else { // Check if the insertion point is valid
			checkPosition(offset);
		}
		// Try to get the counterpart code if it is requested
		ITag opposit = null;
		if ( connect && ( tagType != TagType.STANDALONE )) {
			if ( tagType == TagType.OPENING ) {
				opposit = tags.getClosingTag(id);
			}
			else { // TagType.CLOSING
				opposit = tags.getOpeningTag(id);
			}
			if ( opposit == null ) {
				if ( !allowOrphan ) {
					throw new InvalidParameterException(
						String.format("Cannot add closing/opening code because opening/closing code for id '%s' does not exist.", id));
				}
			}
		}
		// Create the new code
		ICode code;
		switch ( tagType ) {
		case OPENING:
			if ( opposit == null ) code = new StartCode(null, TagType.OPENING, id, data);
			else code = new StartCode((EndCode)opposit, data);
			break;
		case CLOSING:
			if ( opposit == null ) code = new EndCode(null, TagType.CLOSING, id, data);
			else code = new EndCode(opposit==null ? null : (StartCode)opposit, data);
			break;
		case STANDALONE:
		default:
			code = new LoneCode(id, data);
			break;
		}
		// Add the new code to the collection and add its reference in the coded text
		if ( offset == -1 ) ct.append(U.toRef(tags.add(code)));
		else ct.insert(offset, U.toRef(tags.add(code)));
		// Set the type only if not null, to avoid overriding type already defined
		// when this new code is created with a counterpart.
		if ( type != null ) {
			code.setType(type);
		}
		return code;
	}
	
	@Override
	public ICode append (TagType tagType,
		String type, 
		String data)
	{
		return insert(tagType, type, null, data, -1, (tagType==TagType.CLOSING), false);
	}
	
	@Override
	public ICode appendStandaloneCode (String type,
		String data)
	{
		return insert(TagType.STANDALONE, type, null, data, -1, false, false);
	}

	@Override
	public ICode openCode (String type,
		String data)
	{
		// Always create a new opening code
		return insert(TagType.OPENING, type, null, data, -1, false, false);
	}

	@Override
	public ICode closeCode (String id,
		String data)
	{
		// Always assume this is closing an existing opening code.
		return insert(TagType.CLOSING, null, id, data, -1, true, false);
	}

	@Override
	public IMarker openMarker (String id,
		String type)
	{
		StartMarker anno = new StartMarker(id, type);
		ct.append(U.toRef(tags.add(anno)));
		return anno;
	}

	@Override
	public IMarker closeMarker (String id) {
		ITag opening = tags.getOpeningTag(id);
		if ( opening == null ) {
			throw new InvalidParameterException(
				String.format("Cannot add closing marker tag because id '%s' does not exist.", id));
		}
		EndMarker ea = new EndMarker((StartMarker)opening);
		ct.append(U.toRef(tags.add(ea)));
		return ea;
	}

	@Override
	public void insert (int pos,
		String plainText)
	{
		checkPosition(pos);
		ct.insert(pos, plainText);
	}

	private void checkPosition (int pos) {
		if ( pos > 0 ) {
			if ( U.isChar1(ct.charAt(pos-1)) ) {
				throw new InvalidPositionException (
					String.format("Position %d is inside a tag reference.", pos));
			}
		}
		else if ( pos < 0 ) {
			throw new InvalidPositionException (
				String.format("Position %d is invalid.", pos));
		}
	}

	@Override
	public int changeToCode (int start,
		int end,
		TagType tagType,
		String id)
	{
		if ( end == -1 ) end = ct.length();
		checkPosition(start);
		checkPosition(end);
		// Check for annotations
		for ( int i=start; i<end; i++ ) {
			char ch = ct.charAt(i);
			if (( ch == C.MARKER_OPENING ) || ( ch == C.MARKER_CLOSING ) || ( ch == C.PCONT_STANDALONE )) {
				throw new RuntimeException("You must not have annotation markers within the span of content to change to code.");
			}
		}
		int lenBefore = ct.length();
		String span = ct.substring(start, end);
		if ( span.isEmpty() ) return 0; // Nothing to do
		StringBuilder sf = new StringBuilder();
		StringBuilder data = new StringBuilder();
		// Move the tags within the span into the new one
		for ( int i=start; i<end; i++ ) {
			char ch = ct.charAt(i);
			if ( U.isChar1(ch) ) { // Must be a code
				int key = U.toKey(ch, ct.charAt(++i));
				ICode code = (ICode)tags.get(key);
				if ( code.getSubFlows() != null ) {
					sf.append((sf.length()>0 ? " " : "")+code.getSubFlows());
				}
				if ( code.getData() != null ) {
					data.append(code.getData());
				}
				// Remove this tag (any paired tag remaining simply becomes orphan)
				tags.remove(key);
			}
			else {
				data.append(ch);
			}
		}
		
		// Create the id if needed
		if ( id == null ) {
			id = tags.getStore().suggestId(false);
		}
		
		// Create the new code
		ICode newCode;
		switch ( tagType ) {
		case OPENING:
			EndCode ec = (EndCode)tags.getClosingTag(id);
			newCode = new StartCode((ec==null ? null : ec.cc), tagType, id, null);
			break;
		case CLOSING:
			StartCode sc = (StartCode)tags.getOpeningTag(id);
			newCode = new EndCode((sc==null ? null : sc.cc), tagType, id, null);
			break;
		case STANDALONE:
		default:
			newCode = new LoneCode(id, null);
			break;
		}
		// Set the data and sub-flows
		if ( sf.length() > 0 ) newCode.setSubFlows(sf.toString());
		if ( data.length() > 0 ) newCode.setData(data.toString());
		int key = tags.add(newCode);
		// Replace the span by the tag reference
		ct.replace(start, end, U.toRef(key));
		// return the difference in length
		return ct.length()-lenBefore;
	}

}
